//
// PomReader
//
// Copyright (C) jextra.net.
//
//  This file is part of the net.jextra.dep Library.
//
//  The net.jextra.dep Library is free software; you can redistribute it and/or
//  modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The net.jextra.dep Library is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the net.jextra.dep Library; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.dep.pom;

import java.io.*;
import java.util.*;
import net.jextra.dep.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;

/**
 * <p>
 * Used by {@link PomExplorer} to read a single pom.xml file.
 * </p>
 */
public class PomReader extends DefaultHandler
{
    // ============================================================
    // Enums
    // ============================================================

    private enum State
    {
        ROOT( "*ROOT*" ),
        UNKNOWN( "*UNKNOWN*" ),
        project( "project" ),

        parent( "parent" ),
        parentGrpId( "groupId" ),
        parentArtifactId( "artifactId" ),
        parentVersion( "version" ),

        groupId( "groupId" ),
        artifactId( "artifactId" ),
        version( "version" ),
        scope( "scope" ),
        optional( "optional" ),
        name( "name" ),
        packaging( "packaging" ),

        dependencies( "dependencies" ),
        dependency( "dependency" ),
        depGroupId( "groupId" ),
        depArtifactId( "artifactId" ),
        depVersion( "version" ),
        depScope( "scope" ),
        depOptional( "optional" ),

        dependencyManagement( "dependencyManagement" ),

        profiles( "profiles" ),
        profile( "profile" ),
        profileId( "id" ),
        activation( "activation" ),
        activeByDefault( "activeByDefault" ),

        exclusions( "exclusions" ),
        exclusion( "exclusion" ),
        excGroupId( "groupId" ),
        excArtifactId( "artifactId" ),

        properties( "properties" ),
        property( "property" );

        private String tag;

        private State( String tag )
        {
            this.tag = tag;
        }

        public String tag()
        {
            return tag;
        }
    }

    // ============================================================
    // Fields
    // ============================================================

    public static final String POM_FILENAME = "pom.xml";
    public static final String POM_EXT = ".pom";

    private Stack<State> stateStack;
    private boolean inTemplate;

    private DepComponent component;
    private DepDependency activeDep;
    private StringBuilder chars;
    private String parentGroupId;
    private DepDependency templateDep;

    private Collection<Property> properties;
    private Property activeProperty;

    private Collection<Profile> profiles;
    private Profile activeProfile;

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public static String getGroupId( String name )
    {
        int slash = name.lastIndexOf( '/' );

        return name.substring( 0, slash );
    }

    public static String getArtifactId( String name )
    {
        int slash = name.lastIndexOf( '/' );

        return name.substring( slash + 1 );
    }

    public void read( InputStream in )
        throws Exception
    {
        assert in != null : "in can not be null.";

        component = new DepComponent();
        properties = new ArrayList<Property>();
        profiles = new ArrayList<Profile>();
        inTemplate = false;

        //
        // Do actual SAX parsing
        //
        parse( in );

        //
        // Process "activeByDefault" profiles
        //
        for ( Profile profile : profiles )
        {
            if ( !profile.getActiveByDefault() )
            {
                continue;
            }

            for ( DepDependency dep : profile.getDependencies() )
            {
                component.addDependency( dep );
            }
        }

        //
        // Fill in groupId and version from template if not specified.
        //
        if ( component.getVersionNumber() == null && templateDep != null )
        {
            component.setVersionNumber( templateDep.getVersionPattern() );
        }
        if ( !component.getName().contains( "/" ) )
        {
            component.setName( buildNameFromGroupId( parentGroupId, component.getName() ) );
        }

        //
        // Add jar file.
        //
        component.addFile( new DepComponentFile( component, String.format( "%s-%s%s",
            getArtifactId( component.getName() ), component.getVersionNumber(), ".jar" ) ) );
    }

    public DepComponent getComponent()
    {
        return component;
    }

    public DepDependency getTemplate()
    {
        return templateDep;
    }

    public Collection<Property> getProperties()
    {
        return properties;
    }

    public void parse( InputStream in )
        throws IOException, SAXException
    {
        stateStack = new Stack<State>();
        stateStack.push( State.ROOT );

        XMLReader reader = XMLReaderFactory.createXMLReader();
        reader.setFeature( "http://xml.org/sax/features/namespaces", false );
        reader.setErrorHandler( new ReaderErrorHandler() );
        reader.setContentHandler( this );
        reader.parse( new InputSource( in ) );
    }

    @Override
    public void startDocument()
        throws SAXException
    {
    }

    @Override
    public void endDocument()
        throws SAXException
    {
    }

    @Override
    public void startElement( String uri, String tagName, String qName, Attributes atts )
        throws SAXException
    {
        State state = getState();
        State newState = State.UNKNOWN;

        if ( matches( State.ROOT, qName, State.project ) )
        {
            newState = State.project;
        }
        else if ( matches( State.project, qName, State.parent ) )
        {
            newState = State.parent;

            templateDep = new DepDependency();
            //            component.setRootDependency( parentDep );
        }
        else if ( matches( State.parent, qName, State.parentGrpId ) )
        {
            newState = State.parentGrpId;
        }
        else if ( matches( State.parent, qName, State.parentArtifactId ) )
        {
            newState = State.parentArtifactId;
        }
        else if ( matches( State.parent, qName, State.parentVersion ) )
        {
            newState = State.parentVersion;
        }
        else if ( matches( State.project, qName, State.groupId ) )
        {
            newState = State.groupId;
        }
        else if ( matches( State.project, qName, State.artifactId ) )
        {
            newState = State.artifactId;
        }
        else if ( matches( State.project, qName, State.version ) )
        {
            newState = State.version;
        }

        else if ( matches( State.project, qName, State.profiles ) )
        {
            newState = State.profiles;
        }
        else if ( matches( State.profiles, qName, State.profile ) )
        {
            newState = State.profile;

            activeProfile = new Profile();
            profiles.add( activeProfile );
        }
        else if ( matches( State.profile, qName, State.profileId ) )
        {
            newState = State.profileId;
        }
        else if ( matches( State.profile, qName, State.activation ) )
        {
            newState = State.activation;
        }
        else if ( matches( State.activation, qName, State.activeByDefault ) )
        {
            newState = State.activeByDefault;
        }

        else if ( matches( State.project, qName, State.dependencyManagement ) )
        {
            newState = State.dependencyManagement;

            inTemplate = true;
        }
        else if ( matches( State.project, qName, State.dependencies )
            || matches( State.dependencyManagement, qName, State.dependencies )
            || matches( State.profile, qName, State.dependencies ) )
        {
            newState = State.dependencies;

            activeDep = null;
        }

        else if ( matches( State.dependencies, qName, State.dependency ) )
        {
            newState = State.dependency;

            activeDep = new DepDependency();
        }
        else if ( state == State.dependency )
        {
            if ( matches( State.dependency, qName, State.depGroupId ) )
            {
                newState = State.depGroupId;
            }
            else if ( matches( State.dependency, qName, State.depArtifactId ) )
            {
                newState = State.depArtifactId;
            }
            else if ( matches( State.dependency, qName, State.depVersion ) )
            {
                newState = State.depVersion;
            }
            else if ( matches( State.dependency, qName, State.depScope ) )
            {
                newState = State.depScope;
            }
            else if ( matches( State.dependency, qName, State.depOptional ) )
            {
                newState = State.depOptional;
            }
        }
        else if ( matches( State.project, qName, State.properties ) )
        {
            newState = State.properties;

            activeProperty = null;
        }
        else if ( state == State.properties )
        {
            newState = State.property;

            activeProperty = new Property( qName );
            properties.add( activeProperty );
        }

        chars = new StringBuilder();

        pushState( newState );
    }

    @Override
    public void characters( char[] value, int offset, int count )
        throws SAXException
    {
        chars.append( new String( value, offset, count ) );
    }

    @Override
    public void endElement( String uri, String tagName, String qName )
        throws SAXException
    {
        State state = popState();
        String string = chars.toString();

        if ( state == null )
        {
            return;
        }

        switch ( state )
        {
        case groupId:
            component.setName( buildNameFromGroupId( string, component.getName() ) );
            break;

        case artifactId:
            component.setName( buildNameFromArtifactId( string, component.getName() ) );
            break;

        case version:
            component.setVersionNumber( string );
            break;

        case parentGrpId:
            parentGroupId = string;
            templateDep.setName( buildNameFromGroupId( string, templateDep.getName() ) );
            break;

        case parentArtifactId:
            templateDep.setName( buildNameFromArtifactId( string, templateDep.getName() ) );
            break;

        case parentVersion:
            templateDep.setVersionPattern( string );
            break;

        case parent:
            activeDep = null;
            break;

        case dependencyManagement:
            inTemplate = false;
            break;

        case profile:
            activeProfile = null;
            break;

        case depGroupId:
            if ( activeDep != null )
            {
                activeDep.setName( buildNameFromGroupId( string, activeDep.getName() ) );
            }
            break;

        case depArtifactId:
            if ( activeDep != null )
            {
                activeDep.setName( buildNameFromArtifactId( string, activeDep.getName() ) );
            }
            break;

        case depVersion:
            if ( activeDep != null )
            {
                activeDep.setVersionPattern( string );
            }
            break;

        case depScope:
            activeDep.setScope( string );
            break;

        case depOptional:
            activeDep.setOptional( Boolean.parseBoolean( string ) );
            break;

        case dependency:
            if ( activeDep != null )
            {
                if ( inTemplate )
                {
                    component.addTemplateDependency( activeDep );
                }
                else if ( activeProfile != null )
                {
                    activeProfile.addDependency( activeDep );
                }
                else
                {
                    component.addDependency( activeDep );
                }
            }
            activeDep = null;
            break;

        case property:
            activeProperty.setValue( string );
            break;

        case profileId:
            activeProfile.setId( string );
            break;

        case activeByDefault:
            boolean abd = Boolean.parseBoolean( string );
            activeProfile.setActiveByDefault( abd );
            break;
        }
    }

    // ----------
    // protected
    // ----------

    protected State getState()
    {
        if ( stateStack.isEmpty() )
        {
            return null;
        }

        return stateStack.peek();
    }

    protected State popState()
    {
        State state = stateStack.pop();

        return state;
    }

    protected void pushState( State state )
    {
        stateStack.push( state );
    }

    protected String buildNameFromGroupId( String groupId, String name )
    {
        StringBuilder builder = new StringBuilder();
        
        if ( groupId != null )
        {
            builder.append( groupId.replace( '.', '/' ) );
            builder.append( "/" );
        }

        if ( name != null )
        {
            builder.append( name );
        }

        return builder.toString();
    }

    protected String buildNameFromArtifactId( String artId, String name )
    {
        StringBuilder builder = new StringBuilder();
        
        if ( name != null )
        {
            builder.append( name );
        }

        if ( artId != null )
        {
            builder.append( artId );
        }

        return builder.toString();
    }

    // ----------
    // private
    // ----------

    private boolean matches( State currentState, String qName, State newState )
    {
        State state = getState();

        if ( state != currentState )
        {
            return false;
        }

        return qName.equalsIgnoreCase( newState.tag() );
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    public static class Property
    {
        private String key;
        private String value;

        public Property( String key )
        {
            this.key = key;
        }

        public Property( String key, String value )
        {
            this.key = key;
            this.value = value;
        }

        public String getKey()
        {
            return key;
        }

        public String getValue()
        {
            return value;
        }

        public void setValue( String value )
        {
            this.value = value;
        }
    }

    private class Profile
    {
        private boolean activeByDefault;
        private String id;
        private ArrayList<DepDependency> dependencies;

        public Profile()
        {
            dependencies = new ArrayList<DepDependency>();
        }

        public void setId( String id )
        {
            this.id = id;
        }

        public boolean getActiveByDefault()
        {
            return activeByDefault;
        }

        public void setActiveByDefault( boolean active )
        {
            activeByDefault = active;
        }

        public List<DepDependency> getDependencies()
        {
            return dependencies;
        }
        
        public void addDependency( DepDependency dep )
        {
            dependencies.add( dep );
        }

        @Override
        public String toString()
        {
            return String.format( "[%s] {%b}", id, activeByDefault );
        }
    }

    private class ReaderErrorHandler implements ErrorHandler
    {
        @Override
        public void warning( SAXParseException ex )
            throws SAXException
        {
            throw ex;
        }

        @Override
        public void error( SAXParseException ex )
            throws SAXException
        {
            throw ex;
        }

        @Override
        public void fatalError( SAXParseException ex )
            throws SAXException
        {
            throw ex;
        }
    }
}
